博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
基于Node的React图片上传组件实现
阅读量:4086 次
发布时间:2019-05-25

本文共 26056 字,大约阅读时间需要 86 分钟。

写在前面

红旗不倒,誓把JavaScript进行到底!今天介绍我的开源项目  里的图片上传组件的前后端实现原理(ReactNode),花了一些时间,希望对你有所帮助。http://blog.csdn.net/u011413061/article/details/52169321

这里写图片描述

前端实现

遵循React 组件化的思想,我把图片上传做成了一个独立的组件(没有其他依赖),直接import即可。

1     
2
3
4
5
6
7
8
9
10
import React, { Component }       
from
'react'
import Upload
from
'../../components/FormControls/Upload/'
//......
render() {
return (
)
}

uri 参数是必须传的,是图片上传的后端接口地址,接口怎么写下面会讲到。

渲染页面

组件render部分需要体现三个功能:

  • 图片选取(dialog窗口)
  • 可拖拽功能(拖拽容器)
  • 可预览(预览列表)
  • 上传按钮 (button)
  • 上传完成图片地址和链接 (信息列表)

render函数

1     
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
render() {     
return (
this.handleChange(v)}
type="file"
size={this.state.size}
name="fileSelect"
accept="image/*"
multiple={this.state.multiple} />
this.handleDragHover(e)}
onDragLeave={(e)=>this.handleDragHover(e)}
onDrop={(e)=>this.handleDrop(e)}
className="upload-drag-area">
或者将图片拖到此处
{ this._renderPreview(); // 渲染图片预览列表 }
{ this._renderUploadInfos(); // 渲染图片上传信息 }
)
}

渲染图片预览列表

1     
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
_renderPreview() {     
if (
this.state.files) {
return
this.state.files.map(
(item, idx) => {
return (

{item.name}

{this.state.progress[idx]}
)
})
} else {
return null
}
}

渲染图片上传信息列表

1     
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_renderUploadInfos() {     
if (
this.state.uploadHistory) {
return
this.state.uploadHistory.map(
(item, idx) => {
return (

上传成功,图片地址是:
查看

);
})
} else {
return null;
}
}

文件上传

前端要实现图片上传的原理就是通过构建FormData对象,把文件对象append()到该对象,然后挂载在XMLHttpRequest对象上 send() 到服务端。

获取文件对象

获取文件对象需要借助 input 输入框的 change 事件来获取 句柄参数 e

1
onChange={(e)=>      
this.handleChange(e)}

然后做以下处理:

1     
2
3
4
5
6
7
8
9
10
11
12
13
e.preventDefault()     
let target = event.target
let files = target.files
let count =
this.state.multiple ? files.length :
1
for (
let i =
0; i < count; i++) {
files[i].thumb = URL.createObjectURL(files[i])
}
// 转换为真正的数组
files =
Array.prototype.slice.call(files,
0)
// 过滤非图片类型的文件
files = files.filter(
function (file) {
return
/image/i.test(file.type)
})

这时 files 就是 我们需要的文件对象组成的数组,把它 concat 到原有的 files里。

1
this.setState({      
files:
this.state.files.concat(files)})

如此,接下来的操作 就可以 通过 this.state.files 取到当前已选中的 图片文件。

利用Promise处理异步上传

文件上传对于浏览器来说是异步的,为了处理 接下来的多图上传,这里引入了 Promise来处理异步操作:

1     
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
upload(file, idx) {     
return
new
Promise(
(resolve, reject) => {
let xhr =
new XMLHttpRequest()
if (xhr.upload) {
// 上传中
xhr.upload.addEventListener(
"progress", (e) => {
// 处理上传进度
this.handleProgress(file, e.loaded, e.total, idx);
},
false)
// 文件上传成功或是失败
xhr.onreadystatechange =
(e) => {
if (xhr.readyState ===
4) {
if (xhr.status ===
200) {
// 上传成功操作
this.handleSuccess(file, xhr.responseText)
// 把该文件从上传队列中删除
this.handleDeleteFile(file)
resolve(xhr.responseText);
}
else {
// 上传出错处理
this.handleFailure(file, xhr.responseText)
reject(xhr.responseText);
}
}
}
// 开始上传
xhr.open(
"POST",
this.state.uri,
true)
let form =
new FormData()
form.append(
"filedata", file)
xhr.send(form)
})
}

上传进度计算

利用XMLHttpRequest对象发异步请求的好处是可以 计算请求处理的进度,这是fetch所不具备的。
我们可以为 xhr.upload 对象的 progress 事件添加事件监听:

1     
2
3
4
xhr.upload.addEventListener(      
"progress", (e) => {
// 处理上传进度
this.handleProgress(file, e.loaded, e.total, i);
},
false)

说明:idx参数是纪录多图上传队列的索引

1     
2
3
4
5
6
handleProgress(file, loaded, total, idx) {     
let percent = (loaded / total *
100).toFixed(
2) +
'%';
let _progress =
this.state.progress;
_progress[idx] = percent;
this.setState({
progress: _progress })
// 反馈到DOM里显示
}

拖拽上传

拖拽文件对于HTML5来说其实非常简单,因为它自带的几个事件监听机制可以直接做这类处理。主要用到的是下面三个:

1     
2
3
onDragOver={(e)=>      
this.handleDragHover(e)}
onDragLeave={(e)=>
this.handleDragHover(e)}
onDrop={(e)=>
this.handleDrop(e)}

取消拖拽时的浏览器行为:

1     
2
3
4
handleDragHover(e) {     
e.stopPropagation()
e.preventDefault()
}

处理拖拽进来的文件:

1     
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
handleDrop(e) {     
this.setState({
progress:[]})
this.handleDragHover(e)
// 获取文件列表对象
let files = e.target.files || e.dataTransfer.files
let count =
this.state.multiple ? files.length :
1
for (
let i =
0; i < count; i++) {
files[i].thumb = URL.createObjectURL(files[i])
}
// 转换为真正的数组
files =
Array.prototype.slice.call(files,
0)
// 过滤非图片类型的文件
files = files.filter(
function (file) {
return
/image/i.test(file.type)
})
this.setState({
files:
this.state.files.concat(files)})
}

多图同时上传

支持多图上传我们需要在组件调用处设置属性:

1     
2
multiple = {       
true }
// 开启多图上传
size = {
50 }
// 一次最大上传数量(虽没有上限,为保证服务端正常,建议50以下)

然后我们可以使用 Promise.all() 处理异步操作队列:

1     
2
3
4
5
6
7
handleUpload() {     
let _promises =
this.state.files.map(
(file, idx) =>
this.upload(file, idx))
Promise.all(_promises).then(
(res) => {
// 全部上传完成
this.handleComplete()
}).catch(
(err) => {
console.log(err) })
}

好了,前端工作已经完成,接下来就是Node的工作了。

后端实现

为了方便,后端采用的是express框架来快速搭建Http服务和路由。具体项目见我的github 。逻辑虽然简单,但还是有几个可圈可点的地方:

跨域调用

本项目后端采用的是express,我们可以通过 res.header() 设置 请求的 “允许源” 来允许跨域调用:

1
res.header(      
'Access-Control-Allow-Origin',
'*');

设置为 * 说明允许任何 访问源,不太安全。建议设置成 你需要的 二级域名,如 jafeney.com

除了 “允许源” ,其他还有 “允许头” 、”允许域”、 “允许方法”、”文本类型” 等。常用的设置如下:

1     
2
3
4
5
6
7
function allowCross(res) {     
res.header(
'Access-Control-Allow-Origin',
'*');
res.header(
'Access-Control-Allow-Headers',
'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
res.header(
'Access-Control-Allow-Methods',
'PUT, POST, GET, DELETE, OPTIONS');
res.header(
"X-Powered-By",
' 3.2.1')
res.header(
"Content-Type",
"application/json;charset=utf-8");
}

ES6下的Ajax请求

ES6风格下的Ajax请求和ES5不太一样,在正式的请求发出之前都会先发一个 类型为 OPTIONS的请求 作为试探,只有当该请求通过以后,正式的请求才能发向服务端。

所以服务端路由 我们还需要 处理这样一个 请求:

1     
2
3
4
5
6
7
8
router.options(      
'*',
function (req, res, next) {
res.header(
'Access-Control-Allow-Origin',
'*');
res.header(
'Access-Control-Allow-Headers',
'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
res.header(
'Access-Control-Allow-Methods',
'PUT, POST, GET, DELETE, OPTIONS');
res.header(
"X-Powered-By",
' 3.2.1')
res.header(
"Content-Type",
"application/json;charset=utf-8");
next();
});

注意:该请求同样需要设置跨域。

处理上传

处理上传的图片引人了multiparty模块,用法很简单:

1     
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*使用multiparty处理上传的图片*/     
router.post(
'/upload',
function(req, res, next) {
// 生成multiparty对象,并配置上传目标路径
var form =
new multiparty.Form({
uploadDir:
'./public/file/'});
// 上传完成后处理
form.parse(req,
function(err, fields, files) {
var filesTmp =
JSON.stringify(files,
null,
2);
var relPath =
'';
if (err) {
// 保存失败
console.log(
'Parse error: ' + err);
}
else {
// 图片保存成功!
console.log(
'Parse Files: ' + filesTmp);
// 图片处理
processImg(files);
}
});
});

图片处理

Node处理图片需要引入 gm 模块,它需要用 npm 来安装:

1
npm install gm --save

BUG说明

注意:node的图形操作gm模块前使用必须 先安装 imagemagick 和 graphicsmagickLinux (ubuntu)上使用apt-get 安装:

1     
2
sudo apt-get install imagemagick     
sudo apt-get install graphicsmagick --with-webp // 支持webp格式的图片

MacOS上可以用 Homebrew 直接安装:

1     
2
brew install imagemagick     
brew install graphicsmagick --with-webp // 支持webp格式的图片

预设尺寸

有些时候除了原图,我们可能需要把原图等比例缩小作为预览图或者缩略图。这个异步操作还是用Promise来实现:

1     
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function reSizeImage(paths, dstPath, size) {     
return
new
Promise(
function(resolve, reject) {
gm(dstPath)
.noProfile()
.resizeExact(size)
.write(
'.' + paths[
1] +
'@' + size +
'00.' + paths[
2],
function (err) {
if (!err) {
console.log(
'resize as ' + size +
' ok!')
resolve()
}
else {
reject(err)
};
});
});
}

重命名图片

为了方便排序和管理图片,我们按照 “年月日 + 时间戳 + 尺寸” 来命名图片:

1     
2
var _dateSymbol =       
new
Date().toLocaleDateString().split(
'-').join(
'');
var _timeSymbol =
new
Date().getTime().toString();

至于图片尺寸 使用 gm的 size() 方法来获取,处理方式如下:

1     
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gm(uploadedPath).size(      
function(err, size) {
var dstPath =
'./public/file/' + _dateSymbol + _timeSymbol
+
'_' + size.width +
'x' + size.height +
'.'
+ _img.originalFilename.split(
'.')[
1];
var _port = process.env.PORT ||
'9999';
relPath =
'http://' + req.hostname + ( _port!==
80 ?
':' + _port :
'' )
+
'/file/' + _dateSymbol + _timeSymbol +
'_' + size.width +
'x'
+ size.height +
'.' + _img.originalFilename.split(
'.')[
1];
// 重命名
fs.rename(uploadedPath, dstPath,
function(err) {
if (err) {
reject(err)
}
else {
console.log(
'rename ok!');
}
});
});

总结

对于大前端的工作,理解图片上传的前后端原理仅仅是浅层的。我们的口号是 “把JavaScript进行到底!”,现在无论是ReactNative的移动端开发,还是NodeJS的后端开发,前端工程师可以做的工作早已不仅仅是局限于web页面,它已经渗透到了互联网应用层面的方方面面,或许,叫 全栈工程师 更为贴切吧。

当然,全栈 两个字的分量很重,不积跬步,无以至千里,功力低下的我还需要不断修炼和实践!

参考


@欢迎关注我的  和 

你可能感兴趣的文章
JavaScript专题之数组去重
查看>>
Immutable.js 以及在 react+redux 项目中的实践
查看>>
Vue2.0全家桶仿腾讯课堂(移动端)
查看>>
React+Redux系列教程
查看>>
react-native 自定义倒计时按钮
查看>>
19 个 JavaScript 常用的简写技术
查看>>
ES6这些就够了
查看>>
微信小程序:支付系列专辑(开发指南+精品Demo)
查看>>
iOS应用间相互跳转
查看>>
iOS开发之支付宝集成
查看>>
iOS开发 支付之银联支付集成
查看>>
iOS开发支付集成之微信支付
查看>>
浅谈JavaScript--声明提升
查看>>
React非嵌套组件通信
查看>>
Websocket 使用指南
查看>>
浏览器兼容性问题解决方案 · 总结
查看>>
一个很棒的Flutter学习资源列表
查看>>
为什么你应该放弃React老的Context API用新的Context API
查看>>
Flutter 布局控件完结篇
查看>>
Koa2初体验
查看>>